#!/usr/bin/python -u
# -*- coding: utf-8 -*-


import os,sys,email,email.FeedParser,ConfigParser,tempfile,smtplib,re,logging
from poplib import POP3
from pprint import pprint
from time import sleep,time
from threading import Thread,Timer,Semaphore
from popen2 import Popen4
import GPG2

body_pending="""A thread has stopped writing to stdout/stderr. Maybe it's waiting for input.
Here's the output of this thread :
"""

body_ended="""The execution of sended commands is finished. Here's the output produced :
"""

SPECIAL_CMD='MAIL2SHELL'

# sequence number => no replay
lts_sem=Semaphore()
last_timestamp=0 ## /!\ THIS PERMIT PAQUET REPLAY /!\ only for debug !!!
# last_timestamp=int(time())

# executing thread list
tl_sem=Semaphore()
thread_list=[]

class treatMail(Thread):
   def __init__ (self,msg,cfg):
      """Constructor
      in msg: email message
      in cfg: configuration dict
      """
      Thread.__init__(self)
      self.setDaemon(True)
      self.msg = msg # email to be treated
      self.cfg = cfg # config dictionary
      self.output=[] # stdout & stderr of commands
      self.gpg = GPG2.GPG() # GNUpg object
      self.log = logging.getLogger(self.getName())

   def isSigned(self,msg):
      """check if a message is signed with gpg
      in msg: email
      return dict from gpg.decrypt
      """
      if (msg.is_multipart()):
         self.log.debug('Mime message')
         # TODO: mime, not treated yet
         return {'valid':0}
      # validate sig & decrypt email
      sig = self.gpg.decrypt( msg.get_payload(),passphrase=self.cfg['passphrase'] )
      self.log.debug(sig.__dict__)
      return sig.__dict__

   def isValid(self,d):
      """Test validity of a returned dict from isSigned
         in d: dict from isSigned
         return boolean,reason(str)
            Valid signature
            Signature key is autorized
            timestamp is > last received message
            crypt is on and message crypt or crypt off
         """
      global last_timestamp,lts_sem
      why="" # text reason of failure
      #pprint(d)
      # test validity
      lts_sem.acquire()
      ret = d['valid'] == 1 \
        and d['key_id'] == self.cfg['sender_key'] \
        and int(d['timestamp']) > last_timestamp \
        and ((self.cfg['crypt'] and d['ok']) \
              or not self.cfg['crypt'] )
      lts_sem.release()
      if ret:
         # update last timestamp
         lts_sem.acquire()
         last_timestamp = int(d['timestamp'])
         lts_sem.release()
      else:
         # create reason of failure
         if d['valid'] != 1:
            why += "not valid signature|"
         if d['key_id'] != self.cfg['sender_key']:
            why += "key_id not recognized %s|"%d['key_id']
         if int(d['timestamp']) <= last_timestamp:
            why += "timestamp too old (%d<=%d)|"%(int(d['timestamp']),last_timestamp)
         if not ((self.cfg['crypt'] and d['ok']) or not self.cfg['crypt'] ):
            why += "crypt config problem (%d,%d)|"%(self.cfg['crypt'],d['ok'])
      self.log.debug("Validity: %s,%s"%(ret,why))
      return ret,why

   def sendEmail (self,server,fro,to,subject,body,body_header):
      """send result email
      in serve: smtp server
      in fro: From: header
      in to: To: header
      in subject: Subject: header
      in body: Body of the email
      in body_header: bosy to be inserted before
      """
      self.log.info("Sending mail from "+fro+" to "+to)
      if self.cfg['crypt']:
         # encrypt the email
         body_signed = self.gpg.encrypt(body_header+"".join(body),
                                        self.cfg['sender_key'],
                                        sign=self.cfg['user'],
                                        passphrase=self.cfg['passphrase'])
      else:
         # only sign the email
         body_signed = self.gpg.sign(body_header+"".join(body),
                                     keyid=self.cfg['user'],
                                     passphrase=self.cfg['passphrase'])
      # construct the message
      msg = ("From: %s\nTo: %s\nSubject: Execution %s\n\n%s"
               %(fro,to,subject,str(body_signed)))
      # send it
      s = smtplib.SMTP(server)
      #~ s.set_debuglevel(1)
      s.sendmail(fro, to, msg)
      s.quit()
   
   def threadPendingForever(self):
      """thread will be considered as dead, kill bash and all childs
      """
      self.log.warning("waiting for too long, ending")
      self.output.append("\n> execution too long KILLING\n")
      # kill every children
      for pid in Popen4("ps -o pid --no-headers --ppid "+str(self.child.pid)).fromchild.readlines():
         os.kill(int(pid.strip()),15)
         os.kill(int(pid.strip()),9)
      # kill main process (bash)
      os.kill(self.child.pid,15)
      os.kill(self.child.pid,9)
   
   def threadPending(self):
      """send email to user if thread is pending, asking for input"""
      global body_ended
      self.log.info("pending "+self.msg['Subject'])
      # send the email
      self.sendEmail(self.cfg['smtp_host'],self.msg['To'],self.msg['From'],
                     self.getName()+" pending : "+self.msg['Subject'],
                     self.output,body_pending)
   
   def specialCommand(self,cmd):
      """execute a special command
      cmd: string : SPECIAL_CMD command
      return self.output (list)"""
      global thread_list,tl_sem
      # get the command
      rcmd = cmd[len(SPECIAL_CMD):].strip().upper()
      self.log.info("special command "+rcmd)
      self.output.append("> "+rcmd+"\n")
      # switch(command):
      if rcmd == 'PUBKEY':
         # send public key
         self.output.append(self.gpg.export_key(self.cfg['user']))
      elif rcmd == 'LIST':
         # list executing threads
         tl_sem.acquire()
         for thread in thread_list:
            self.output.append("%s\t%s\t%s\n"%(thread.getName(),thread.msg['From'],thread.msg['Subject']))
         tl_sem.release()
      elif rcmd.startswith('SHOW'):
         # show output of a running thread
         thread_arg = rcmd.split(' ')[1] # argument: thread name
         tl_sem.acquire()
         for thread in thread_list:
            # find concerned thread
            if thread.getName().upper() == thread_arg:
               self.output.append("%s\t%s\t%s\n%s\n"%(thread.getName(),thread.msg['From'],
                                                      thread.msg['Subject'],
                                                      "".join(thread.output)))
         tl_sem.release()
      else:
         # display help
         self.output += ["Special commands: MAIL2SHELL <command> [args]\n",
                         "HELP\t: display help\n",
                         "PUBKEY\t: Get public key\n",
                         "LIST\t: List running threads\n",
                         "SHOW <ThreadName>\t: Show current output of ThreadName\n"]
      return self.output
   
   def execCmds (self,cmds):
      """Execute a list of shell commands
         input: string
         return self.output (list)"""
      # write commands to temp file
      f = tempfile.NamedTemporaryFile(dir='/tmp/')
      f.write(cmds)
      f.flush()
      #~ os.system ("cat "+f.name)
      # add script at the beginning
      self.output.append(re.compile("^",re.MULTILINE).sub("> ",cmds)+"\n\n")
      # exec commands
      # start a timer for max exec time
      bigT = Timer(self.cfg['execution'],self.threadPendingForever)
      bigT.start()
      # execute the script
      self.child = Popen4("bash "+f.name)
      while True:
         # start the timer for one line
         t = Timer(self.cfg['timeout'],self.threadPending)
         t.start()
         line = self.child.fromchild.readline()
         t.cancel()
         if not line:
            break
         self.output.append(line)
      bigT.cancel()
      return self.output

   def sendInput (self,msg):
      """send input from msg to sdtin"""
      # verify signature
      d = self.isSigned(msg)
      isV,reason = self.isValid(d)
      if isV:
         self.log.info("Message input '%s' valid, sending ..."%(msg['Subject']))
         self.output.append(d['data']) # also write to child output (echo) ??
         self.child.tochild.write(d['data'])
         self.child.tochild.flush()
      else:
         self.log.warning("Message input '%s' NOT valid (%s)"%(msg['Subject'],reason))

   def run(self):
      global body_ended,thread_list,tl_sem
      d = self.isSigned(self.msg)
      isV,reason=self.isValid(d)
      if isV:
         self.log.info("Message '%s' valid, execution ..."%(self.msg['Subject']))
         # test for special commands
         if d['data'].startswith(SPECIAL_CMD):
            # treat only first line
            retour = self.specialCommand(d['data'].split('\n')[0])
         else:
            retour = self.execCmds(d['data'])
         #~ print retour
         self.sendEmail(self.cfg['smtp_host'],self.msg['To'],self.msg['From'],
                        self.msg['Subject'],
                        retour,body_ended)
         self.log.info("Ended")
      else:
         self.log.warning("Message '%s' NOT valid (%s)"%(self.msg['Subject'],reason))
      # remove thread from thread list
      tl_sem.acquire()
      thread_list.remove(self)
      tl_sem.release()


class controlApp:
   def __init__(self,cfg):
      self.cfg = cfg
      self.childs=[]
      self.log = logging.getLogger('control')
      
   def run(self):
      while True:
         # get mails and treat them
         self.log.info("Connecting to pop server ...")
         msgs = self.getMails(self.cfg['user'],self.cfg['passwd'],self.cfg['pop_host'])
         self.log.info("%d messages"%(len(msgs)))
         self.treatMails(msgs)
         self.log.debug("Sleeping for %.0f sec ..."%(self.cfg['delay']))
         sleep(self.cfg['delay'])
   
   def treatMails(self,msgs):
      # check validity of messages
      global thread_list,tl_sem
      self.log.debug("go throw messages ...")
      existing_thread = False
      for msg in msgs:
         tl_sem.acquire()
         for thread in thread_list:
            if msg['Subject'].find(thread.getName()) != -1:
               # find existing thread name in subject, this is an input
               existing_thread = True
               self.log.info("find existing thread "+thread.getName())
               thread.sendInput(msg)
         tl_sem.release()
         if not existing_thread:
            # new command
            current = treatMail(msg,self.cfg)
            tl_sem.acquire()
            thread_list.append(current)
            tl_sem.release()
            current.start()
         sleep(1) # wait a bit between messages

   def getMails(self,user,passwd,host):
      """return list of messages (email objects)"""
      l=[]
      srv = POP3(host)
      srv.user(user)
      srv.pass_(passwd)
      srv.list()
      (numMsgs, totalSize) = srv.stat()
      for i in range(1, numMsgs + 1):
         (header, msg, octets) = srv.retr(i)
         srv.dele(i) # delete mail
         fp = email.FeedParser.FeedParser()
         fp.feed("\n".join(msg)+"\n")
         l.append(fp.close())
      srv.quit()
      return l


if __name__ == "__main__":
   
   # get user/pass/server ## OBSOLETE
   #~ user=sys.argv[1].split(':')[0]
   #~ passwd=sys.argv[1].split(':')[1].split('@')[0]
   #~ host=sys.argv[1].split('@')[1]
   
   if len(sys.argv) < 2:
      print "usage: %s <config_file>"%sys.argv[0]
      sys.exit(2)
   cfg_dict={}
   cfg = ConfigParser.ConfigParser()
   cfg.read(sys.argv[1])
   cfg_dict['user']=cfg.get("General","user")
   cfg_dict['passwd']=cfg.get("General","password")
   cfg_dict['pop_host']=cfg.get("General","pop")
   cfg_dict['sender_key']=cfg.get("General","sender_key")
   cfg_dict['smtp_host']=cfg.get("General","smtp")
   cfg_dict['delay']=cfg.getfloat("General","delay")
   cfg_dict['timeout']=cfg.getfloat("General","timeout")
   cfg_dict['execution']=cfg.getfloat("General","execution")
   cfg_dict['crypt']=cfg.getboolean("General","crypt")
   cfg_dict['passphrase']=cfg.get("General","passphrase")

   # log configuration
   log_file=cfg.get("General","log_file")
   log_level=cfg.getint("General","log_level")
   # log to file
   logging.basicConfig(level=log_level*10,
                    filename=log_file,
                    filemode='w')
   # also log to console
   logging.getLogger('').addHandler(logging.StreamHandler())

   # log configuration dict
   logging.info(cfg_dict)
   
   controlApp(cfg_dict).run()

